온라인 주문 서비스를 서버리스 아키텍쳐로 구축하기 #분산트랜잭션 #실시간데이터스트리밍
본 포스팅은 지난 3/28(토) 온라인으로 개최된 JAWS DAYS 2020 의 등단을 위해 작성한 일본어 글의 번역본입니다.
JAWS DAYS 2020 에서의 발표와 더불어, COVID-19 로 인해 현지 개최가 취소된 AWS Summit Seoul 2020 의 커뮤니티 트랙에서 본 포스팅의 내용으로 발표할 예정이었으나, 아쉽게도 행사가 취소되는 바람에 블로그로 원래 발표하려고 했던 내용을 공유합니다! 자연스러운 한국어 뉘앙스를 전달하기 위해 원본과는 사뭇 다른 표현을 사용하는 경우가 있습니다 :)
원문 보기
JAWS DAYS 2020 등단 페이지
注文サービスをサーバーレスで作ってみた
시작하며
안녕하세요! 오랜만의 포스팅이네요. 클래스메소드 의 김태우입니다! 항상 한국어 포스팅의 시작에 저희회사 홍보도 할 겸 입사 1개월차에 작성한 블로그 링크를 달아두는데요, 이제 한국에 계신 분들도 꽤 많은 분들이 저희 회사에 대해 알게되신 것 같아서 뿌듯함을 느끼고 있습니다. 이제 오는 4월 1일부터는 한국에서 신입사원분들이 6명이 입사하시고, 베트남에서도 2명이 입사하게 되어서 꽤나 글로벌한 환경에서 근무하게 될 것 같습니다! 물론, 현재로서는 100% 리모트 근무이기 때문에 같은 공간에서 일하는 것은 나중 일이 되겠지만요... '-';; (클래스메소드에서는 언제나 한국인 엔지니어분들을 모시고 있습니다! 자세한 내용은 채용페이지를 참고해주세요.)
본 포스팅에서는 AWS 상에서 분산 트랜잭션을 손쉽게 구축할 수 있는 AWS Step Functions 의 use case 에 대해 소개하고, 트랜잭션 결과를 손쉽게 클라이언트로 스트리밍 할 수 있는 AppSync 의 use case 에 대해서도 소개합니다. 마지막으로, 실제로 이를 사용하여 데모 서비스를 구현한 Github 리포지토리를 공개합니다.
그럼, 시작합니다!
트랜잭션이란?
트랜잭션에 대해 처음 접하시는 분들을 위한 섹션입니다. 트랜잭션에 대해 익숙하신 분들은 곧바로 다음 섹션으로 스킵하셔도 됩니다.
트랜잭션을 쉽게 이해할 수 있는 예를 하나 들어서 설명하겠습니다. 해외여행을 가본 적 없는 철수는 연말휴가를 계획하고 있습니다.(코로나 제발요..ㅠㅠ) 철수는 올해야말로 반드시 해외여행을 가겠노라고 다짐합니다. 해외여행을 위해 준비해야하는 것들에 대해 알아보니, 아래의 항목들을 반드시 준비해야한다는 것을 알게되었습니다.
해외여행을 위해 반드시 준비해야하는 것들
- 여권발급 신청
- (필요에 의한)VISA 신청
- 항공편 예약
- 숙박 예약
물론, 이것들 외에도 여러가지 더많은 항목들이 있겠지만, 일단 여기서는 위의 4가지만 고려합니다. 위의 4가지 항목들이 전부 잘 준비된다면 행복한 여행을 할 수 있겠지만, 만에 하나 한가지 항목이라도 준비가 되지 않는다면, 기다리고 기다리던 철수의 첫 해외여행은 어떤 모습이 될까요?
만약 여권을 발급받지 못하고 공항에 간다면, 여행은 커녕 출국심사도 받지 못하게 됩니다. VISA 발급이 되지않은 상태에도 역시나 현지에 도착했더라도, 입국심사가 거절당하게 될 확률이 매우 큽니다. 비행기 예약이 되어 있지 않은 경우에는 두 말 할 것도 없겠죠.
이렇듯, 해외여행을 가고싶다고 생각하더라도, 실제로 준비해야하는 위의 4가지 항목 중에 단 하나라도 클리어하지 못한다면 여행 승인 상태 가 OK 가 되지 않습니다. 위의 예시의 경우, 여권, VISA, 항공편, 숙박의 4가지 태스크에 대한 결과가 전부 OK가 되는 경우에 한하여, 여행 승인 상태 가 OK 가 됩니다. 그 외의 경우에는 모두 여행 승인 상태 가 NG 가 된다는 것을 알 수 있습니다.
이 예시를 소프트웨어의 관점에서 생각해본다면 어떨까요? 일련의 흐름을 아래의 그림으로 표현할 수 있습니다.(참고로, 아래의 그림은 AWS Step Functions 에서 간단한 스크립트를 작성만 해도 생성할 수 있습니다)
이러한 흐름을 그림으로 그려놓고 보니, 개발자분들에게 익숙한 플로우차트가 만들어졌습니다. 위의 플로우차트에서 지금 예시로 들고 있는 상황을 고려해보았을때, 가장 중요한 포인트는 바로 Start 부터 End 까지 반드시 완전하게 실행이 시작되고 종료되어야한다는 점입니다. 도중에 맘대로 소프트웨어가 멈춰버리거나, 중지되면 안된다는 거죠. 예를 들어, 비행기의 예약은 되었는데, 숙박 예약을 하지 않은채로 소프트웨어가 동작해버리면, 여행을 가게 되더라도 숙박할 곳이 없어서 난처한 상황이 됩니다. 혹은, 비행기의 예약은 되었는데, 숙박이 되지 않아서 전체 프로세스가 취소되는 상황에서, 오작동으로 인해 항공편의 예약이 취소되지 않고 종료되어도 돈을 날리게 되어 굉장히 난감한 상황이 됩니다.
이와 같이,
모든 태스크가 성공하던지, 모든 태스크가 실패하던지의 두가지의 결과 밖에 없는 일련의 태스크의 처리과정
을 트랜잭션이라고 정의할 수 있습니다.
분산 트랜잭션이란?
위의 예를 모놀리식 아키텍쳐로 개발하는 경우에는, 이러한 트랜잭션은 DB 레벨에서의 트랜잭션 처리로 해결가능한 경우가 많습니다. 특히나, ORM 등의 라이브러리를 이용하면, session.flush()
, session.commit()
등의 메소드로 트랜잭션 처리가 굉장히 간편해집니다. MySQL 이나 PostgreSQL 을 많은 웹서비스의 DB 로 사용하는 이유 중의 하나가 바로 트랜잭션에 대한 강력한 지원이 있기 때문이기도 합니다.
그렇지만, 마이크로서비스 아키텍쳐에서는 이러한 DB 레벨에서의 트랜잭션만으로 트랜잭션을 처리하는게 상당히 어려워집니다. 각 서비스마다 DB 가 분리되어 있는 경우가 일반적이고, 심지어는 서비스마다 전혀 별개의 DB 엔진을 사용하는 등(MySQL, DynamoDB 등)의 데이터베이스 레이어에서의 트랜잭션에 의존할 수 없는 상황이 되어버립니다. 혹은, 외부의 서드파티 서비스와의 통신도 트랜잭션에 포함되어야하는 경우도, 모놀리식 아키텍쳐 때 보다 마이크로 서비스 아키텍쳐에서 더욱 일반적으로 수용되고 있는 것 같습니다.
즉, 분산 트랜잭션 의 방법론에 대한 고려가 필요하게 되었습니다. 분산 트랜잭션의 방법론으로써 여러가지 접근법이 있지만, 본 포스팅에서는 Sagas 패턴을 발전시킨 Distributed Sagas 패턴에 대해 소개하겠습니다.
Distributed Sagas 란?
Distributed Sagas 패턴이란 Caitie McCaffrey 가 고안한 분산 트랜잭션 알고리즘입니다. 주로 서드파티 시스템을 트랜잭션의 참가자로서 고려하고 있는 알고리즘이지만, 마이크로 서비스의 철학상, 다른 서비스들을 원래 모두 서드파티처럼 생각하기 때문에 마이크로 서비스 아키텍쳐에서도 굉장히 잘 적용될 수 있는 방법론입니다.
아래 Youtube 영상을 보시면 Caitie McCaffrey 가 직접 Distributed Sagas 에 대해 발표하고 있는 영상을 확인하실 수 있습니다.
Distributed Sagas 를 한마디로 표현하자면,
일단, 각각의 태스크를 수행한다. 만약 어떤 태스크가 실패한다면, 이미 수행된 태스크를 무효처리할 수 있는 태스크를 반드시 실행시킨다.
적고보니 두마디네요..(ㅋㅋ) 여튼, 여기서 「이미 수행된 태스크를 무효처리할 수 있는 태스크」를「보상(compensation)태스크」라고 부릅니다.
보다 구체적인 예와 함께 자세한 설명은 아래 데모 서비스를 설명하면서 함께 설명하고 있으니 본 포스팅의 마지막까지 읽어주세요 :)
Step Functions 과 Distributed Sagas
여기서부터가 진짜입니다!ㅎㅎ
Distributed Sagas 를 실제로 개발하려면 트랜잭션의 각각의 태스크를 컨트롤하는 Saga Execution Coordinator(SEC) 의 신뢰성 및 확장성을 확보하는 것이 가장 중요한 포인트 중 하나입니다. 물론, 가장 어려운 작업이기도 할 것입니다. SEC 에 문제가 발생한다면, 애초에 이러한 알고리즘 자체가 의미가 없어지기 때문입니다.
여기서 바로!!! AWS Step Functions 가 완전관리형 서비스로서 활용가능하지 않을까 하고 이미 몇년전부터 대두되던 글들이 있습니다.
- How the Saga Pattern manages failures with AWS Lambda and Step Functions
- Distributed Sagas for Microservices
추측이지만, 아마도 이러한 Step Functions 의 use case 를 AWS 에서도 공식적으로 받아들여서, 올초에 아래의 sample 리포지토리를 공개한 것이 아닐까 하는 생각이 듭니다.
AWS Step Functions 란, 완전관리형 서버리스 서비스로 제공되는 워크플로우 관리툴입니다.
굉장히 좋은 서비스임에도 불구하고 아직 많은 분들에게 잘 알려지지 않거나, 아직 사용해보신 분들이 적은 서비스 중에 하나이지 않을까 개인적으로 생각하고 있습니다. 본 포스팅에서는 이 AWS Step Functions 가 분산트랜잭션을 위한 Distributed Sagas 의 구현에 최적의 서비스라는 것을, 특히, 온라인 주문 서비스에 사용될 수도 있는 활용가치가 높은 서비스라는 것을 소개하고자 합니다.
참고) Apache Airflow 역시 AWS Step Functions 와 함께 비교대상이 되는데요, AWS Step Funcitons 와 비교하면, 학습 코스트를 포함하여 초기도입 코스트가 꽤 높다는 특징이 있습니다. (ECS 나 Kubernetes 의 차이같은..) AWS Step Functions 의 경우, 도입을 위해 선행하여 학습할 내용이 (비교적) 거의 없고, 초기 도입 비용이 거의 발생하지 않고도 신뢰성과 안정성이 높은 서비스를 구축할 수 있게 도와주기때문에, Airflow 를 도입해야하는 이유가 없다면 가벼운 마음으로 사용할 수 있는 서비스이지 않을까 생각합니다.
AWS AppSync 와 리얼타임 데이터 스트리밍
일반적으로 트랜잭션을 구현해야하는 기능의 경우, 비즈니스의 핵심적인 부분을 담당하고 있는 기능일 가능성이 높다고 생각합니다. 따라서, 이 트랜잭션의 결과를 즉시 알고 싶은 경우도 많을 것 같다고 생각합니다. 따라서 본 포스팅의 데모에서는 주문 트랜잭션이 성공했을 경우에 한하여 관리자가 확인할 수 있을법한 어드민 페이지에서 그 결과를 거의 리얼타임으로 받아올 수 있는 아키텍쳐를 소개하고 있습니다.
그 리얼타임 통신을 위해 본 데모에서 사용하고 있는 서비스가 바로 AWS AppSync 입니다.
AWS AppSync 는 완전관리형 GraphQL 서비스로써, 서버리스 서비스로 제공됩니다. AWS 와 더불어 GraphQL 을 굉장히 좋아하는 저는 AppSync 를 너무너무 사랑하지만, 아직 AppSync 를 사용한 공개사례가 한국에서는 많지 않다고 느끼고 있어서 아쉬움이 있습니다. 애초에 GraphQL 을 이해하고 있는 개발자들을 대상으로 한 서비스로 보여지는 점도 도입을 망설이게하는 이유에 큰 기여를 한다고 생각하고 있습니다.
그래서!!!ㅎㅎ GraphQL 에 대하여 전혀 모르는 사람도 쉽게 적용할 수 있는 use case 를 본 데모에서 소개하고자 합니다. 단순히 리얼타임 데이터 스트리밍 만을 위한 기능으로 도입해도 훌륭한 서비스라고 생각하는 만큼, 많은 분들이 제 포스팅을 통하여 AppSync 의 유용한 use case 에 대해 알게 되셨으면 합니다.
리얼타임 데이터 스트리밍을 위한 AWS AppSync 활용전략에 대해서는 제가 AWS 서버리스 소모임에서 발표했던 자료가 있으므로 아래 슬라이드를 확인해주세요.
또한, AWS AppSync 의 개념 및 간단한 튜토리얼을 작성한 포스팅도 있으므로, 관심있으신 분들은 읽어보시면 좋을것 같습니다 :)
DEMO 아키텍쳐 소개
먼저 데모 영상을 보신 후에 설명드리겠습니다!
영상의 처음 화면에서 왼쪽이 유저 대상의 앱, 오른쪽이 쇼핑몰의 관리자가 확인하는 어드민 프로젝트라고 생각해주세요. (어드민으로 안보여도 어드민으로 봐주세요....ㅎㅎㅎ)
- 앱은 Angular 기반의 Ionic 프레임워크 을 사용하고 있습니다.
- 결제기능은 Stripe 를 사용하고 있습니다. (한국 화폐는 지원하지 않습니다..ㅠㅠ)
- 노란색 영역은 주문서비스에 관한 아키텍쳐, 파란색 부분은 리얼타임 데이터 스트리밍에 관한 아키텍쳐입니다.
- Step Functions 의 Lambda 에서 AppSync 를 호출할때, 본 데모에서 실제로는 SQS 를 통해 연계됩니다.
- AWS Step Functions 의 워크플로우는 아래의 플로우차트를 확인해주세요.
아키텍쳐 해설
전체적인 데모 서비스의 흐름을 명확히 하기 위해 위의 그림을 그려보았습니다.
- 하드코딩된 서버리스 관련 서적리스트 중 하나를 선택합니다.
- 신용카드 정보를 입력합니다.(Stripe페이지에서 테스트용의 카드정보가 공개되어있습니다.)
- 클라이언트에서 사전 주문 리퀘스트를 보냅니다.
- 백엔드(주문서비스)에서 PaymentIntent 오브젝트를 생성하여 클라이언트에게 리스폰스합니다. 이 오브젝트에 client_secret 정보가 생성되어 있어서, 이 정보를 사용하여 클라이언트 측에서 Stripe 에 ConfirmPayment 리퀘스트를 보낼 수 있게 됩니다.
- 클라이언트에서 ConfirmPayment 리퀘스트를 보냅니다.
- 그림의 6a, 6b, 6c 는 실행순서와 무관합니다. 6a. 결제결과를 클라이언트에게 알려줍니다. 6b. 결제 결과를 백엔드에 webhook 트리거시켜줍니다. Stripe webhook 은 라이브결제의 경우에만 트리거 시켜주므로, 본 데모에서는 클라이언트에서 직접 백엔드에 webhook 이벤트를 트리거시켜주고 있습니다. 6c. 클라이언트는 페이지를 이동시킵니다.
- 트랜잭션이 끝났는지를 1초 주기로 확인합니다.(Step Functions 는 비동기로 동작하므로, 결과를 확인하기 위해 주기적으로 체크하거나, CloudWatch Event 를 통해 이벤트 드리븐 방식으로 접근하는 선택지가 있지만, 본 데모에서는 1초 단위로 체크하는 방식으로 코드를 작성하였습니다.)
- 트랜잭션이 끝났으면 유저에게 결과를 보여줍니다.
Step Functions 의 마지막에 SendOrderConfirmEvent 태스크가 있는데요, 여기서는 트랜잭션의 결과를 스트리밍 시키기 위해 AppSync 에 CreateOrder Mutation 을 실행시킵니다. 아래 그림은 AppSync 에 의한 스트리밍 과정을 나타냅니다.
개인적으로는 이 AppSync 에 의한 리얼타임 데이터 스트리밍을 굉장히 좋아하는데요, 그 이유가 개발자나 인프라 담당자가 실제로 작업해야할 양이 거의 제로에 가까워지기 때문입니다. 구체적으로 말하면, 「데이터 타입을 정의해놓는 것만으로도 확장성, 신뢰성, 안정성을 갖춘 리얼타임 서비스를 개발할 수 있기 때문」입니다. 물론, 서비스의 사양에 따라 이 "리얼타임" 이라고 하는 기준이 다르게 해석될 가능성도 있겠지만, 일반적인 SNS 기능 등과 같은 엄격한 리얼타임 요건이 필요로 하지 않는 서비스에서는 AppSync 가 최적의 선택지 중에 하나라고 생각합니다. 흔히 Redux 나 ngrx 같은 state management 라이브러리 등을 활용하여 클라이언트 사이드에서도 데이터 플로우나 데이터 스테이트 관리 등을 하게되는데요, (코드가 상당히 복잡해집니다..) 이러한 경우에도 AppSync 를 사용하면, 굳이 이러한 스테이트 관리를 해주지 않아도 간단하게 서버와 클라이언트의 데이터가 동기화 됩니다. 좀 더 구체적으로 말하면, AppSync 클라이언트 라이브러리 를 활용하여 subscription 을 불러주기만 하면 됩니다. (AppSync 를 클라이언트에서 사용하는 경우, AppSync Client Library 와 Amplify 를 고려할 수 있는데, Amplify 를 권장합니다만, 본 데모에서는 AppSync Client Library 를 활용하여 개발하였습니다.)
예를 들어, 본 데모의 어드민 프로젝트에서 실제로 사용한 코드는 아래와 같습니다.
const subscribeOrder = gql` subscription onCreateOrder { onCreateOrder { id paymentId createdAt itemId title subtitle price expiresAt } } `; let subscription; let self = this; (async () => { subscription = this.appSyncClient.subscribe({ query: subscribeOrder }).subscribe({ next: data => { const item = data.data.onCreateOrder; self.items.push(item); }, error: error => { console.warn(error); } }); })();
백엔드에서는 아무것도 하지 않아도 됩니다. GraphQL 의 타입을 정의만 해두면, 클라이언트에서 위와같은 코드를 어딘가에 작성해두기만 하면 리얼타임 기능이 완성됩니다. (제가 AppSync 를 너무나 좋아하는 큰 이유중의 하나이지만, 이 외에도 AppSync 는 멋진 기능을 많이 가지고 있으므로 사용해보시는 것을 강력히 추천합니다!ㅎㅎ)
소스코드 (Github URL)
Github
twkiiim/jawsdays2020-demo-serverless-order-service
본 데모는 4가지의 프로젝트로 구성되어 있습니다.
- 어드민용 Angular 프로젝트
- 유저가 사용할 Ionic 프로젝트 (Angular 기반)
- 백엔드 주문 서비스
- 백엔드 AppSync 서비스
jawsdays2020-demo-admin/ ## 일반적인 Angular 프로젝트 구성 jawsdays2020-demo-app/ ## 일반적인 Ionic 프로젝트 구성 order/ ## 주문서비스 프로젝트 api/ startOrder/ postPayment/ checkOrderStatus/ manage/ create_resoures.py model/ order.py util/ node_modules/ package.json requirements.txt serverless.yml ## Step Functions 관련 설정은 여기에 venv/ yarn.lock stream/ ## AppSync 서비스 프로젝트 schema/ order.graphql resolvers/ Mutation.createOrder.request.vtl Mutation.createOrder.response.vtl streamToAppSync.js serverless.yml package.json node_modules/ yarn.lock
개발환경
본 데모에서는 아래의 툴이나 프레임워크를 사용하고 있으므로 버전 확인이나, 설치되어 있지 않은 툴이나 프레임워크의 설치가 필요합니다.
- virtualenv
- yarn (or npm)
- AWS CLI
- Docker
- python (3.7 이상)
- Node.js (10.x 이상)
- Angular
- Ionic
- Serverless
$ yarn --version 1.17.3 $ npm --version 6.13.4 $ node --version v12.16.1 $ serverless --version Framework Core: 1.57.0 ... $ vertualenv --version 16.6.1 $ aws --version aws-cli/1.16.294 Python/3.7.4 Darwin/19.2.0 botocore/1.13.30 $ docker --version Docker version 19.03.5, build 633a0ea
jawsdays2020-demo-admin 프로젝트
$ cd jawsdays2020-demo-admin/ $ ng version Angular CLI: 9.0.7 Node: 12.16.1 OS: darwin x64 Angular: 9.0.7 ... animations, cli, common, compiler, compiler-cli, core, forms ... language-service, platform-browser, platform-browser-dynamic ... router Ivy Workspace: Yes Package Version ----------------------------------------------------------- @angular-devkit/architect 0.900.7 @angular-devkit/build-angular 0.900.7 @angular-devkit/build-optimizer 0.900.7 @angular-devkit/build-webpack 0.900.7 @angular-devkit/core 9.0.7 @angular-devkit/schematics 9.0.7 @ngtools/webpack 9.0.7 @schematics/angular 9.0.7 @schematics/update 0.900.7 rxjs 6.5.4 typescript 3.7.5 webpack 4.41.2
jawsdays2020-demo-app 프로젝트
$ cd jawsdays2020-demo-app $ ionic version 5.4.16 $ ng version Angular CLI: 8.3.25 Node: 12.16.1 OS: darwin x64 Angular: 8.2.14 ... common, compiler, compiler-cli, core, forms ... language-service, platform-browser, platform-browser-dynamic ... router Package Version ----------------------------------------------------------- @angular-devkit/architect 0.803.25 @angular-devkit/build-angular 0.803.25 @angular-devkit/build-optimizer 0.803.25 @angular-devkit/build-webpack 0.803.25 @angular-devkit/core 8.3.25 @angular-devkit/schematics 8.3.25 @angular/cli 8.3.25 @ngtools/webpack 8.3.25 @schematics/angular 8.3.25 @schematics/update 0.803.25 rxjs 6.5.4 typescript 3.4.5 webpack 4.39.2
인스톨 방법
위의 Github 리포지토리에서 본 데모 프로젝트를 clone 해 옵니다.
$ git clone https://github.com/twkiiim/jawsdays2020-demo-serverless-order-service.git
주문 서비스
먼저 virtualenv 를 생성하고, pip install 합니다.
$ cd order/ $ virtualenv venv $ source venv/bin/activate (venv) $ pip install -r requirements.txt
yarn 커맨드로 serverless framework plugin 을 인스톨합니다.
(venv) $ yarn
위의 yarn 커맨드로 아래의 플러그인이 인스톨 됩니다.
- serverless-python-requirements
- serverless-step-functions
다음은 serverless.yml 파일을 열고, provider > profile
에 본인의 aws profile 이름을 입력해주세요.
manage/create_resources.py 를 실행시켜서 필요한 리소스를 생성합니다.
(venv) $ cd manage/ (venv) $ python create_resources.py
위 커맨드로 SQS Queue 와 DynamoDB 테이블이 생성됩니다.
마지막으로 sls deploy 로 리소스를 디플로이합니다.
$ sls deploy -v
AppSync 서비스
먼저, yarn 커맨드로 디펜던시를 인스톨합니다.
$ cd stream/ $ yarn
위의 yarn 커맨드를 실행하면 아래의 라이브러리 혹은 플러그인이 인스톨됩니다.
- aws-appsync
- graphql
- graphql-tag
- isomorphic-fetch
- serverless-appsync-plugin
serverless.yml 를 열어서 아까 주문 서비스를 디플로이 할 때처럼 provider > profile
를 변경하고, functions > streamToAppSync > events > sqs > arn
에 아래의 커맨드를 실행시켜서 얻은 SQS Queue ARN 을 적어줍니다.
$ aws sqs aws sqs list-queues ## https://********.queue.amazonaws.com/************/jawsdays2020_demo_order_queue 를 복사해옵니다. $ aws sqs get-queue-attributes --queue-url "위에서 얻어온 Queue URL" --attribute-names QueueArn
AppSync 액세스 정보를 얻기 위해 일단 이대로 디플로이 합니다.
$ sls deploy -v
디플로이가 완료되면 AppSync Endpoint(GraphQlApiUrl)、API Key(GraphQlApiKeyDefault) 가 표시됩니다. 이것들을 복사해서
streamToAppSync.js 의 url
、apiKey
변수에 적어줍니다.
다시한번 디플로이합니다.
$ sls deploy -v
참고) 이러한 귀찮은 작업을 하면서 저도「별로 좋지 않은」디플로이 방법이라고 느꼈습니다. 본 데모 뿐만아니라 의존관계가 있는 서비스를 동시에 디플로이 해야할 때는 AWS CDK 가 좋을 것 같다는 생각이 들었습니다.
Ionic 프로젝트
첫번째로 디플로이시킨 주문서비스(order/)에서 생성된 API Gateway 엔드포인트를 src/app/service/payment.service.ts
에 기재합니다.
Stripe 키는 공개테스트 키를 사용하고 있으므로 그대로 두셔도 됩니다.
$ cd jawsdays2020-demo-app/ $ yarn $ ionic serve
자동으로 브라우저가 열리면서, Ionic 프로젝트가 보여지면 성공입니다!
어드민용 Angular 프로젝트
AppSync 의 액세스 정보 부분을 수정하고 실행합니다. src/app/appsync.connector.ts
파일을 열고, APPSYNC_ENDPOINT
、AWS_REGION
、API_KEY
의 부분을 실제 데이터값으로 바꿔줍니다. 끝으로, ng serve 를 실행합니다.
$ cd jawsdays2020-demo-admin/ $ yarn $ ng serve
위의 4가지 프로젝트를 실제로 로컬 및 AWS 상에서 실행시켜보면 생각보다 굉장히 재밌을겁니다!ㅎㅎㅎ
마치며
더 많은 분들이 AWS Step Functions 와 AWS AppSync 에 관심을 갖고 사용했으면 하는 바램으로 본 포스팅을 작성하였습니다. AWS 에는 이와 같은 완전관리형 서비스가 굉장히 많은데요, 이러한 서비스들을 실제 서비스에 사용하는 것이 아니더라도, 그냥 혼자서 이것저것 직접 사용해보고 공부하는 것만으로도 충분히 즐겁다고 생각합니다!ㅎㅎ 그리고 평소에 이렇게 다양한 서비스들을 직접 사용해보지 않으면 필요한 순간에 어떤 서비스가 어떨 때 좋고, 어떨 때 적합하지 않은지 판단할 수가 없게 됩니다. 완전관리형 서비스라서 좋은 점도 있지만, 직접 구축해서 완전한 컨트롤을 할 수 있는 것과 비교해서 어느정도의 제약사항이 붙는 것은 어쩔 수가 없습니다. 하지만, 이러한 제약사항의 범위를 넘지 않는 선에서 사용가능한 use case 가 있다면, 이러한 완전관리형 서비스를 1000% 활용해서 신뢰성과 안정성이 높은 서비스를 믿기지 않는 속도로 개발할 수도 있습니다. 또한, 이러한 서비스를 활용하면서 개발할 때, 개발하는 즐거움도 빼놓을 수가 없습니다.
AWS Step Functions 이나 AWS AppSync 도 마찬가지로 실제로 활용 가능한 기능이 많은 서비스이므로, 이번 기회에 많은 분들이 캐치업하셔서 실제 서비스에 다양하게 적용하는 사례들이 많이 나왔으면 합니다.
끝으로, 본 데모 서비스를 개발할 때에 주의점이나 설명이 부족한 부분들에 대해 부록 섹션에서 다루는 것으로 본 포스팅을 마치고자 합니다. COVID-19 관련해서도 건강 조심하시고, 항상 기쁜 일이 넘치시기를 기원합니다! :D
부록
트랜잭션이 실패한 경우
Distributed Sagas 패턴은 기본적으로 보상(compensation)이라는 형태로, 트랜잭션이 실패한 경우의 처리를 수행합니다. 데모에서는 RequestDelivery
와 CancelDelivery
태스크가 랜덤함수로 인해 랜덤하게 실패함으로써 실제 서비스 환경에서 실패한 경우를 모사하고 있습니다. (음.. 뭔가 카오스 엔지니어링같은 접근법이네요ㅎㅎ)
위의 플로우차트는, 랜덤하게 RequestDelivery
가 실패하여, 보상 태스크가 실행되어 전체적인 트랜잭션이 성공적으로 종료된 (실패처리된) 경우의 상태머신입니다. 그런데, 만에 하나라도 이 보상 태스크가 실행되는 중에 실패하게 된다면 어떻게 되는 걸까요? 이러한 경우 역시 모사하기 위해 CancelDelivery
태스크를 랜덤으로 실패시켜 보았습니다.
여기서부터는 여러가지 접근법이 가능하다고 생각하지만, 개인적으로 프로그래밍 영역에서의 접근보다는 "모니터링" 이라는 영역에서의 해결책을 선호합니다. 본 데모에서는 AWS 를 사용한다면 가장 기본적인 모니터링 어프로치가 되는 CloudWatch Event 를 사용하여 Step Functions 가 실패했을 경우에 알람이 오도록 설정해보았습니다.
혹시라도 CloudWatch Event 를 아직 사용해보지 않으신 분이 계시다면, 위 사진과 같이 CloudWatch Event 를 사용하면 모니터링 설정이 이렇게 간단하게 설정할 수 있습니다. 이벤트 발생후 연계될 타겟으로는 SNS 의 test-topic
이라는 토픽에 붙여놓고, 실제로 해당 토픽에 subscribe 하고 있는 제 메일로 알림이 오는지 확인해보았습니다.
메일의 형식이 알아보기 어렵지만, Lambda 등을 통해 데이터를 알기 쉽게 가공하여 메일로 보내는 방식도 충분히 간단하게 가능하며, 메일이 아닌 Slack 등의 메세지로 연결되도록 하는 등의 여러가지 활용방법들이 있습니다만, 본 포스팅의 주제에 벗어나기 때문에 생략합니다.
DynamoDB 의 TTL 에 대하여
본 데모의 AWS AppSync 의 DynamoDB 테이블을 확인해보면, expiresAt
이라는 필드가 존재합니다. 이 필드에 TimeToLiveSpecification 가 설정되어 있는데요(serverless.yml), 본 데모의 AppSync 는 단순히 트랜잭션 결과를 스트리밍 시키는 역할 그 이상, 그 이하도 하지 않고 있기 때문에 데이터를 계속해서 저장하고 있을 필요는 없지만, AppSync 와 가장 통합이 간편한 DynamoDB 를 사용하고 있기에 TTL 을 설정함으로서 데이터 저장소의 용량문제를 해결하였습니다. TTL 을 설정해두면, DynamoDB 에서 해당 시각이 되면 자동으로 데이터를 삭제해주기 때문에 데이터 삭제를 위한 배치 등은 별도로 필요하지 않습니다. (정확한 타이밍에 삭제되는 것은 아니므로 다른 use case 에서 사용할때에는 충분한 검증작업이 필요합니다.) 본 데모에서는 stream/streamToAppSync.js 에서, +60초 로 설정해둔 것을 확인하실 수 있습니다. (더 짧은 시간단위로 설정해두어도 전혀 문제없다고 생각합니다.)
AppSync 의 Query (getOrder) 에 대하여
Github 코드상에서 AppSync 의 Schema 를 보면, 본 데모에서 전혀 사용하지 않는 getOrder
라는 쿼리타입이 존재합니다. 이것은 AppSync 의 GraphQL 문법상, 쿼리가 적어도 하나 이상 반드시 필요하게 되는 조건이 있어서, 가장 간단한 형태의 getOrder 쿼리타입 및 resolver 를 추가해두었습니다.
Stripe 의 Webhook 테스트에 관하여
본 포스팅의 데모 개발에 있어서 가장 까다로웠던 부분 중에 하나가 Stripe 의 Webhook 을 테스트하는 것이었습니다. 라이브 결제가 아닌 테스트 결제의 경우, Stripe Webhook 이 트리거 되지 않기 때문에 Webhook 데이터가 어떤 모습을 하고 있을지 상상하는 것이 조금 어려웠습니다. 물론, Stripe 문서에 데이터 형식에 대한 문서가 존재하지만, 실제로 데이터를 받아보고 싶었습니다.
조사해본 결과, Stripe 에서는 Stripe CLI 를 제공하고 있어서, CLI 를 사용하여 webhook 이벤트를 로컬에서 트리거 시킬 수 있었습니다. 따라서 아래의 stripe 커맨드를 실행하여 테스트용 결제가 일어났을때 결제 이벤트를 CLI 상에서 캐치할 수 있었습니다.
$ stripe listen --forward-to localhost:5000/webhook
그리고, 위의 커맨드를 실행한 뒤에 아래와 같은 Flask 서버를 실행시켜서, 작성해두었던 Lambda 핸들러 함수와 연결되도록 설정하는 것으로, 테스트 결제를 라이브 결제 처럼 테스트 해볼 수 있었습니다.
import stripeWebhook import json from flask import Flask from flask import request app = Flask(__name__) @app.route('/webhook', methods=['POST']) def webhook(): event = { 'body': json.dumps(request.json) } result = stripeWebhook.handler(event, None) print(result) return result if __name__ == '__main__': app.run()
참고자료
- Distributed Sagas: A Protocol for Coordinating Microservices - Caitie McCaffrey - JOTB17
- Distributed Sagas for Microservices
- Applying the Saga pattern with AWS Lambda and Step Functions
- AWS Step Functions 유저가이드
- Serverless Frameworkで構築するStep Functions
- Github: yosriady/serverless-sagas
- AppSync를 활용한 리얼타임 서버리스 아키텍쳐